/*
 * @Author: 小花
 * @Date: 2022-04-12 09:29:41
 * @LastEditors: 小花
 * @LastEditTime: 2022-04-17 22:51:45
 * @FilePath: /UserView/Volumes/Document/Gitee/logistics-repair-system/AdminView/vue3-teap/src/plugins/websocket/websocket.ts
 * @Description: 封装socket
 */
/**
 * =========================Use manual=========================
 * 1、Use new the keyword create have a websocket instance.
 *    new CreateSocket()
 *
 * 2、This instance must take two parameters, they are url and message callback.
 *    function callback (e) {}
 *    const socket = new CreateSocket(url, callback)
 *
 * 3、We need to use the 'build' method to build this instance
 *    const socket = new CreateSocket(url, callback).build()
 *      or
 *    const socket = new CreateSocket(url, callback)
 *    const bsocket = socket.build()
 *
 * 4、Building this instance, it will return have two methods. they are 'send' method and 'close' method
 *    socket.send()  It sends messages.
 *    socket.close()  It close socket.
 *
 * ---extend
 *    This the instance it accept the third parameter 'config'
 *    'config' it is a object, this parameter is optional.
 *    This configuration is below, called 'IConfigs'
 *
 * =========================Quick start=========================
 * function onmessage (e) { console.log(e) };
 * const socket = new CreateSocket(url, onmessage).build();
 * setTimeout(function(){ socket.send('This is a message test'); }, 2000);
 * setTimeout(function(){ socket.close(); }, 4000);
 */

enum EReadyState {
  // 连接尚未建立
  connNotCreated = 0,
  // 连接已建立，可以进行通信
  connSuccess = 1,
  // 连接正在进行关闭
  connClosing = 2,
  // 连接已经关闭或者连接不能打开
  connClose = 3
}

enum socketEnum {
  /**
   * socket发送数据
   */
  WEBSOCKET_DATA = 43014,
  /**
   * socket发送ping
   */
  WEBSOCKET_PING = 43015
}

interface socketUtil {
  code: socketEnum,
  msg: string,
  data: any
}

export interface IConfigs {
  // satoken---可为用户名或用户ID
  satoken?: string;
  // 发送心跳间隔时间(毫秒)
  heartTime?: number;
  // 检查链接状态间隔时间(毫秒)
  checkTime?: number;
  // 断线后重启间隔时间(毫秒)
  restartTime?: number;
  // 自动重启websocket
  autoRestart?: boolean;
  // 是否开启心跳
  isOpenHeart?: boolean
}

export class CreateSocket {
  // socket地址
  private static url: string
  // socket会话id
  private satoken: string | undefined
  // socket实例
  private socket: WebSocket | undefined
  // 消息回调
  private message: (ev: MessageEvent<any>) => void
  // ---------心跳机制参数---------
  // 发送心跳间隔时间
  private heartTime = 1000 * 5
  // 触发器
  private heartTrigger: NodeJS.Timer | undefined
  // 心跳控制器
  private heartControl = true
  // 检查链接状态间隔时间
  private checkTime = 3000
  // 触发器
  private checkTrigger: NodeJS.Timeout | undefined
  // 断线后重启间隔时间
  private restartTime = 4000
  // 触发器
  private restartTrigger: NodeJS.Timeout | undefined
  // 重启状态锁（close and error will trigger at the same time）
  private restartLock = false
  // 自动重启（default open auto restart. if close, then appear error won't auto restart websocket）
  private autoRestart = true
  // 是否开启心跳
  private isOpenHeart = true

  /**
   * @description: Create hava a socket instance and build it.
   * @param {string} url ws连接地址
   * @param {function} message 消息回调
   * @param {IConfigs} config 配置项
   * @return {*} WebSocket实例
   */
  constructor (url: string, message: (ev: MessageEvent<any>) => void, config: IConfigs = {}) {
    // 检测浏览器是否支持socket
    this.checkSocket()
    // 初始化socketId, 检测socketId是否为空, 空则动态生成socketId
    // this.satoken = satoken || this.createDynamicSocketId()
    this.handleConfig(config)
    // 初始化url
    CreateSocket.url = this.checkUrl(url)
    // 装配回调
    this.message = message
  }

  /**
   * @description: 检测socket是否支持
   * @param {*}
   * @return {*}
   */
  private checkSocket () {
    if (!('WebSocket' in window)) {
      alert('你的浏览器不支持websocket服务，请升级或更换浏览器')
      throw new Error('你的浏览器不支持websocket服务，请升级或更换浏览器')
    }
  }

  /**
   * @description: 配置项处理器
   * @param {IConfigs} config
   * @return {*}
   */
  private handleConfig (config: IConfigs) {
    const { satoken, heartTime, checkTime, restartTime, autoRestart, isOpenHeart } = config
    this.satoken = satoken || this.createDynamicSocketId()
    if (heartTime && checkTime) {
      if (checkTime - checkTime >= 2000) {
        this.heartTime = heartTime
        this.checkTime = checkTime
      }
    }
    if (heartTime) {
      if (heartTime - this.checkTime >= 2000) {
        this.heartTime = heartTime
      }
    }
    if (checkTime) {
      if (this.heartTime - checkTime >= 2000) {
        this.heartTime = checkTime
      }
    }
    this.restartTime = restartTime || this.restartTime

    if (autoRestart === false) {
      this.autoRestart = autoRestart
    }
    if (isOpenHeart === false) {
      this.isOpenHeart = isOpenHeart
    }
  }

  /**
   * @description: URL处理器
   * @param {string} url
   * @return {*}
   */
  private checkUrl (url: string): string {
    if (!url.trim()) {
      alert('socket链接地址不能为空')
      throw new Error('socket链接地址不能为空')
    }
    url = url.replace(/http:\/\//g, '')
    url = url.indexOf('/', url.length - 1) !== -1 ? (url + this.satoken) : (url + '/' + this.satoken)
    return url
  }

  /**
   * @description: 动态创建socketID(satoken)
   * @param {*}
   * @return {*}
   */
  private createDynamicSocketId () {
    // 编码
    const hash = window.btoa(CreateSocket.url)
    let satoken = sessionStorage.getItem(hash)
    if (satoken) return satoken
    satoken = new Date().getTime().toString() + (Math.random() * 9999 + 1000).toFixed()
    sessionStorage.setItem(hash, satoken)
    return satoken
  }

  /**
   * @description: 构建socket实例对象
   * @param {*}
   * @return {*} { send: 发送消息, close: 关闭socket}
   */
  public build () {
    try {
      this.socket = new WebSocket(CreateSocket.url)
      this.listener()
      return {
        send: this.send,
        close: this.close
      }
    } catch (e: any) { throw new Error(e) }
  }

  /**
   * @description: 消息发送
   * @param {string} message 消息
   * @return {*}
   */
  private send = (message: string | ArrayBufferLike | Blob | ArrayBufferView) => {
    const socketJson: socketUtil = {
      code: socketEnum.WEBSOCKET_DATA,
      msg: 'data',
      data: message
    }
    this.heartCheck();
    (this.socket as WebSocket).send(JSON.stringify(socketJson))
  }

  /**
   * @description: 关闭websocket
   * @param {*} autoRestart 自动重启websocket, defulat close. if open, then after a while websocket auto restart
   * @param {*} restartTime 自动重启时间
   * @return {*} void
   */
  private close = (autoRestart = false, restartTime = this.restartTime) => {
    this.socket && this.socket.close()
    this.socket = undefined
    this.restartLock = true
    this.restartTrigger && clearTimeout(this.restartTrigger)
    this.heartControl = false
    this.heartTrigger && clearInterval(this.heartTrigger)
    this.checkTrigger && clearTimeout(this.checkTrigger)
    this.heartTrigger = this.checkTrigger = this.restartTrigger = undefined
    // 关闭自动重启
    this.autoRestart = autoRestart
    this.restartTime = restartTime
  }

  /**
   * @description: 开启websocket监听
   * @param {*}
   * @return {*}
   */
  private listener () {
    (this.socket as WebSocket).onopen = () => {
      console.log('实时服务连接成功')
      // 连接成功，若控制器为关闭(false)状态，则在监听(listener)前开启心跳控制器
      this.heartControl = true
      this.heartCheck()
    }
    (this.socket as WebSocket).onclose = () => {
      console.warn('实时服务连接关闭')
      // 连接失败，关闭心跳控制器，交由restartSocket处理
      this.heartControl = false
      this.restartSocket()
    }
    (this.socket as WebSocket).onerror = () => {
      console.error('实时服务连接出错')
      // 连接失败，关闭心跳控制器，交由restartSocket处理
      this.heartControl = false
      this.restartSocket()
    }
    (this.socket as WebSocket).onmessage = (ev) => {
      this.message(ev)
    }
  }

  /**
   * @description: 心跳机制处理器
   * @param {*}
   * @return {*}
   */
  private heartCheck () {
    // 是否开启心跳
    if (!this.isOpenHeart) return

    this.heartTrigger && clearInterval(this.heartTrigger)
    this.checkTrigger && clearTimeout(this.checkTrigger)

    if (!this.heartControl) return

    const socketJson: socketUtil = {
      code: socketEnum.WEBSOCKET_PING,
      msg: 'ping',
      data: undefined
    }
    this.heartTrigger = setInterval(() => {
      // 判断当前心跳控制是否关闭 默认开启(true)
      if (!this.heartControl) { this.heartCheck(); return }
      (this.socket as WebSocket).send(JSON.stringify(socketJson))
      this.checkTrigger = setTimeout(() => {
        if ((this.socket as WebSocket).readyState !== EReadyState.connSuccess) (this.socket as WebSocket).close()
      }, this.checkTime)
    }, this.heartTime)
  }

  /**
   * @description: 断线重启处理器
   * @param {*}
   * @return {*}
   */
  private restartSocket () {
    if (this.restartLock && !this.autoRestart) return
    this.restartLock = true
    this.restartTrigger && clearTimeout(this.restartTrigger)

    this.restartTrigger = setTimeout(() => {
      this.build()
      this.restartLock = false
    }, this.restartTime)
  }
}
